/* set ts=4
 *
 * IMPERVIOUS
 *
 ************************************************************************
 *
 * tagslib.c
 *
 * HTML tags parser. This parser is only interested in fetching the links
 * within a web page. It does it in a single pass of the buffer, so there
 * is no need for continual buffering of contents
 *
 * Currently requires that the entire file to be read into memory as there
 * is no context structure to keep track of position (otherwise the
 * consumer code can keep track of the current tag->end offset into the
 * buffer and continue reading the file from there)... this could be
 * implemented later on, but web pages should never be too large anyway so
 * they can usually be read into memory all in one go. Since this is a fast
 * parser, it should complete a file in microseconds (a test file of 89k
 * with 10000 loops -> ~1100/second on a 1.4Ghz CPU).
 *
 * types of HTML tags:
 *   <tag unquoted_attr=value dquoted_attr="value" squoted_attr='value'>
 *   <tag unquoted_attr=value dquoted_attr="value" squoted_attr='value'/>
 *   <script src="src"></script>
 *   <script><!-- script_body --></script>
 *   <script>script_body // line-comment
 *      /-* comment
 *       *
 *       *<>/</script>
 *   <script>do_stuff(); // line-comment</script>
 *   <script>do_stuff(); /-* open comment</script>  <--broken comment
 *   <!-- comment -->
 *   <!-- <enclosed> comment -->
 *   
 *
 * NB: Any changes to this module must be re-entrant.
 * 
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "tagslib.h"

#define skip_whitespace(s) for(;(*s) && *(s)<=' ';s++)
#define is_whitespace(c)   ((c) < ' ')
#define is_alpha(c)        ((c)>='A' && (c)<='Z' || (c)>='a' && (c)<='z')
#define is_alphanum(c)     (is_alpha(c) || ((c)>='0' && (c)<='9'))


int
match_tag_name(char **s)
{
	char *ns;
	char c;
	int i;

	ns = *s;
	while (*ns && is_alpha(*ns)) {
		ns++;
	}
	if (*ns == '\0')
		return (TAG_NONE);

	c = *ns;
	*ns = 0;

	for (i = 0; tag_name[i] != 0; i++) {
		if (strcmp(*s, tag_name[i]) == 0) {
			break;
		}
	}

	*ns = c;
	*s = ns;
	return (i);
}

/*
 * Extracts all the attributes for the tag
 */
char *
extract_attributes(Tag *t, char *s)
{
	char *attr;
	char *val, *vs, *ve;
	char quote;
	int attr_n = 0;
	int tag_type = GET_TAG(t->type);
	int is_end_tag = IS_TAG_END(t->type);

	if (is_end_tag) {
		// skip all the way to the end
		quote = 0;
		while (*s) {
			switch (*s) {
			case '\\':
				if (quote)
					s++;
				break;

			case '"':
			case '\'':
				if (quote == *s)
					quote = 0;
				else if (quote == 0)
					quote = *s;
				break;
			}

			if (*s == '>')
				break;

			if (*s)
				s++;
		}
		t->end = s;
		return (s);
	}

	while (*s) {
		quote = 0;

		// skip whitespace
		skip_whitespace(s);
		if (*s == '\0') {
			break;
		}

		if (*s == '>') {
			t->end = s;
			break;
		}

		attr = s;
		while (*s && is_alphanum(*s))
			s++;

		if (*s == '\0') {
			break;
		}

		if (*s == '>') {
			t->end = s;
			break;
		}

		vs = ve = s;
		if (*s == '=') {
			/*
			 * test if attribute is what we're looking for and extract
			 * the value string from it.
			 */
			char c = *s;
			char *x;
			int i;

			*s = 0;
			for (i = 0; i < MAX_ATTR; i++) {
				x = tag_linkattr[tag_type][i];
				if (!x)
					break;

				if (strcmp(attr, x) == 0) { // found it, fetch the value
					int done;

					val = s + 1;
					if (*val == '\0') {
						*s = c;
						goto fail;
					}

					if (*val == '\'' || *val == '"') {
						quote = *val;
						val++;
					}

					vs = val;
					ve = val;
					done = 0;

					while (!done && *ve) {
						switch (*ve) {
						case '\\':
							if (quote) {
								ve++;
								if (*ve == '\0') {
									done = 1;
								}
							}
							break;

						case '\'':
						case '"':
							if (quote == *ve) {
								quote = 0;
								done = 1;
							}
							break;

						case '>':
						case ' ':
							if (!quote) {
								done = 1;
							}
							break;
						}

						if (!done) {
							ve++;
						}
					} // while (!done && *ve)

					if (*ve) {
						if (attr_n < MAX_ATTR) {
							t->links[attr_n].start = vs;
							t->links[attr_n].end = ve - 1;
						}

						attr_n++;
						if (*ve == '>') {
							*s = c;
							s = ve;
							goto out;
						}
						ve++;
					}

					i = MAX_ATTR;
				} // if (strcmp(attr, x) == 0)
			} // for (i = 0; i < MAX_ATTR; i++)

			// have we found the attribute? if not skip it
			if (ve == s) {
				int done;

				ve = s+1;
				quote = 0;
				if (*ve == '"' || *ve == '\'') {
					quote = *ve;
					ve++;
				}
				for (done = 0; *ve && !done; ) {
					switch (*ve) {
					case '\\':
						if (quote) {
							ve++;
							if (*ve == '\0') {
								done = 1;
							}
						}
						break;

					case '\'':
					case '"':
						if (quote == *ve) {
							quote = 0;
							ve++;
							done = 1;
						}
						break;

					case '>':
					case ' ':
						if (!quote) {
							done = 1;
						}
						break;
					}

					if (!done) {
						ve++;
					}
				}
			}

			*s = c;
			s = ve;
			if (*s == '>') {
				goto out;
			}
		} // if (*s == '=') 

		if (*s) {
			s++;
		}
	} // while (*s)

out:
	return (s);

fail:
	return (t->start);
}

// TODO: parse comments tag for interesting stuff, also
// comment tags could contain CDATA javascript or it could also
// contain < > characters embedded which will break this for now if we
// attempt to parse the comment tag's guts.
char *
parse_tag(Tag *t)
{
	char *s;
	int tag_id;
	char quote = 0;
	int is_end_tag = 0;

	/* skip whitespace */
	s = t->start + 1;
	if (s[0] == '/') {
		// end tag
		is_end_tag = 1;
		s++;
	}
	skip_whitespace(s);

	tag_id = match_tag_name(&s);
	t->type = tag_id;
	if (is_end_tag)
		t->type |= TAG_FLAG_END;
	if (tag_id == TAG_NONE)
		return t->start;

	// continue processing the tag until we've reached to the end of it ('>')
	if (tag_id == TAG_OTHER) {
		// skip to end of tag
		while (*s) {
			switch (*s) {
			case '\\':
				if (quote) {
					s++;
				}
				break;

			case '\'':
			case '"':
				if (quote == *s)
					quote = 0;
				else if (quote == 0)
					quote = *s;
				break;

			case '>':
				// only break out of the tag if no quoted attributes
				if (!quote) {
					goto loopout;
				}
				break;
			}
			if (*s)
				s++;
		}
loopout:
		if (*s == '>') {
			t->end = s;
		}
	} else {
		// extract links within tag
		s = extract_attributes(t, s);
		if (s != t->start) {
			t->end = s;
		}
	}

	return (s);
}

char *
parse_scripted_tag(Tag *t, char quote)
{
	return NULL;
}

void
skip_comments(char **s)
{
	char *x = *s;
	while (*x) {
		if (*x == '-') {
			if (x[1] == '-' && x[2] && x[2] == '>') {
				*s += 2;
				return;
			}
		}
		x++;
	}
	*s = x;
}

Tag
next_tag(char *s, int inscript)
{
	Tag t;
	char *s1;
	char quote = 0;

	memset(&t, 0, sizeof(t));
	if (!*s) {
		goto out;
	}

	if (!inscript) {
		do {
			if (*s == '<') {
				t.start = s;
				if (s[1] == '!' && s[2] && s[2] == '-' && s[3] && s[3] == '-') {
					// this is a comment, skip to end of it
					s += 4;
					skip_comments(&s);
					t.end = s;
					t.type = TAG_COMMENT;
					break;
				} else {
					s1 = parse_tag(&t);
					if (s1 == s) { // if equal, parser failed...
						printf("tag error: %s\n", s1);
						goto out;
					} else {
						goto out;
					}
				}
			}

			s++;
		} while (*s);

		goto out;
	}

	// inscript
	while (*s) {
		switch (*s) {
		case '\\':
			if (quote) {
				// escaped character, skip next
				if (s[1] == '\0') {
					goto out;
				}
				s++;
			}
			break;

		case '\'':
			if (quote == '\'')
				quote = 0;
			else if (quote == 0)
				quote = '\'';
			break;

		case '"':
			if (quote == '"')
				quote = 0;
			else if (quote == 0)
				quote = '"';
			break;

		case '<':
		case '/':
			//
			// This could be:
			//   </  tag end
			//   //  single line comment
			//   /*  multi-line comment
			//
retry_lt:
			s1 = s;
			if (quote) {
				//t.start = s;
				//s = parse_scripted_tag(&t, quote); // TODO

			} else if (s[1] == '/') {
				// is this script end? or is it a comment
				s += 2;
				if (*s == '/') { // it's a comment
					while (*s && *s != '\n') {
						if (*s == '<') {
							goto retry_lt;
						}
						s++;
					}
					if (!s[0]) {
						goto out;
					}
				} else if (*s == '*') { // multiline comment
					for (;;) {
						while (*s && *s != '*') {
							if (*s == '<') {
								goto retry_lt;
							}
							s++;
						}
						if (!s[0]) {
							goto out;
						}
						s++;
						if (*s == '/') {
								break;
						}
					}
				} else if (s1[0] == '<') {
				    // test the tag end... looking for "script"
					int tt;
					tt = match_tag_name(&s);
					if (tt == TAG_SCRIPT) {
						// skip to end of tag
						while (*s && *s != '>') {
							s++;
						}
						t.type = SET_TAG_END(tt);
						t.start = s1;
						t.end = s;
						goto out;
					}
				}
			}
			break;
		} // switch

		if (s[0])
			s++;
	}
out:
	return t;
}



// -D__TAGS_TEST__ to compile with main()
#ifdef __TAGS_TEST__

void dbg_print(char *pre, char *s, char *e)
{
	printf("%s ", pre);
	for (; s <= e; s++) {
		putchar(*s);
	}
	printf("\n");
}

void dbg_nprint(char *pre, char *s, int len)
{
	printf("%s ", pre);
	for (; len > 0; len--) {
		putchar(*s);
		s++;
	}
	printf("\n");
}

char buf[4096*1024];
int main(int argc, char *argv[])
{
	FILE *f;
	Tag t;
	char *s;
	char c;
	int n;
	int inscript = 0;
	int i;

	if (argc == 1)
		return 1;

	f = fopen(argv[1], "rb");
	s = buf;

	while (!feof(f)) {
		n = s-buf;
		n = fread(s, 1, sizeof(buf) - n, f);
		if (n == 0)
			break;
		s += n;
	}

	fclose(f);
	buf[sizeof(buf)-1] = 0;

	n=1; // change this to the profiling loops
	while(n--)

	for (t = next_tag(buf, 0); t.start != 0 && t.end != 0; ) {
		if (GET_TAG(t.type) < TAG_OTHER) {
			printf("[%04x] {%u:%u}: ", t.type, t.start-buf, t.end-buf);
			dbg_print("", t.start, t.end);
			for (i = 0; i < MAX_ATTR && t.links[i].end; i++) {
				dbg_print("                     : ",
				          t.links[i].start, t.links[i].end);
			}
			printf("\n");
		}

		if (t.type == TAG_SCRIPT) {
			inscript = 1;
		} else if (inscript &&
		           IS_TAG_END(t.type) &&
		           GET_TAG(t.type) == TAG_SCRIPT) {
			inscript = 0;
		}

		if (t.end) {
			t = next_tag(t.end+1, inscript);
		}
	}

	return 0;
}

#endif /* __TAGS_TEST__ */
